﻿using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Lua.Converters;
using System.Reflection;

using LuaInterface;

namespace System.Lua.Serialization
{
    
    /// <summary>
    /// Serializes and deserializes object into and from Lua code.
    /// </summary>
    public static class LuaSerializer
    {
        /// <summary>
        /// Serializes an object into a Lua variable.
        /// </summary>
        /// <param name="writer">The <see cref="LuaWriter"/> to use..</param>
        /// <param name="variableName">The name of the variable.</param>
        /// <param name="value">The object to serialize.</param>
        public static void Serialize(LuaWriter writer, string variableName, object value)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");
            if (variableName == null)
                throw new ArgumentNullException("variableName");
            if (value == null)
                throw new ArgumentNullException("value");

            // Serialize the object as a Lua variable assignment
            writer.WriteStartAssignment(variableName);

            // Serialize the actual value of the object
            LuaSerializer.SerializeValue(writer, value);

            // Writes the end of the assignment
            writer.WriteEndAssignment();
        }

        private static void SerializeValue(LuaWriter writer, object value)
        {
            // Get the type of the value
            Type valueType = value.GetType();

            // Determine whether the value should be serialized as a literal, as an array or as a class
            if (valueType.IsLiteral())
            {
                SerializeLiteral(writer, value);
            }
            else if (valueType.IsDictionary())
            {
                SerializeDictionary(writer, value);
            }
            else if (valueType.IsArray)
            {
                SerializeArray(writer, value);
            }
            else if (valueType.IsClass)
            {
                SerializeClass(writer, value);
            }
        }

        private static void SerializeLiteral(LuaWriter writer, object value)
        {
            // Write the value out as a literal expression
            writer.WriteLiteralExpression(value);
        }

        private static void SerializeDictionary(LuaWriter writer, object value)
        {
            // Write the start of the table that will hold the associative array (dictionary)
            writer.WriteStartTable();

            // Iterate through the entries in the dictionary
            foreach (DictionaryEntry entry in (IDictionary)value)
            {
                // Write the start of the associative table field using the entry key
                writer.WriteStartTableField(entry.Key);

                // Serialize the value of the entry
                LuaSerializer.SerializeValue(writer, entry.Value);

                // Write the end of the table field
                writer.WriteEndTableField();
            }

            // Write the end of the table
            writer.WriteEndTable();
        }

        private static void SerializeArray(LuaWriter writer, object value)
        {
            // Write the start of the table that will hold the array items
            writer.WriteStartTable();

            // Iterate through the items in the enumerable array
            foreach (object item in (IEnumerable)value)
            {
                // Write the start of the positional (unnamed) table field
                writer.WriteStartTableField();

                // Serialize the value of the array item
                LuaSerializer.SerializeValue(writer, item);

                // Write the end of the table field
                writer.WriteEndTableField();
            }

            // Write the end of the table
            writer.WriteEndTable();
        }

        private static void SerializeClass(LuaWriter writer, object value)
        {
            // Get the type of the object
            Type valueType = value.GetType();

            // Write the start of the table that holds the object
            writer.WriteStartTable();

            // Iterate through all public properties of the object
            foreach (PropertyInfo propertyInfo in valueType.GetProperties())
            {
                // Check whether the property is decorated with the LuaIgnore attribute, skip if yes
                if (propertyInfo.GetCustomAttributes(typeof(LuaIgnoreAttribute), true).Any())
                    continue;

                // Get the value of the property and determine whether it has one
                object propertyValue = propertyInfo.GetValue(value, null);
                if (propertyValue != null)
                {
                    // Write the start of the named table field that holds the value of the property
                    writer.WriteStartTableField(propertyInfo.Name);

                    // Serialize the value of the property
                    LuaSerializer.SerializeValue(writer, propertyValue);

                    // Write the end of the table field
                    writer.WriteEndTableField();
                }
            }
            
            // Write the end of the table
            writer.WriteEndTable();
        }

        /// <summary>
        /// Deserializes an object from Lua code.
        /// </summary>
        /// <typeparam name="T">The type of the .NET object.</typeparam>
        /// <param name="luaReader"></param>
        /// <param name="variableName">The name of the variable.</param>
        /// <returns>An instance of the <typeparamref name="T"/> type.</returns>
        [SuppressMessage("Microsoft.Design", "CA1004", Justification = "Type parameter is required for deserialization as it cannot be inferred from the Lua representation.")]
        public static T Deserialize<T>(LuaReader luaReader, string variableName)
        {
            if (luaReader == null)
                throw new ArgumentNullException("luaReader");
            if (variableName == null)
                throw new ArgumentNullException("variableName");

            // Deserialize the value of the variable and return it
            return (T)LuaSerializer.DeserializeValue(luaReader[variableName], typeof(T));
        }

        private static object DeserializeValue(object value, Type targetType)
        {
            // If there is no value, return null
            if (value == null)
                return null;

            // If the value's type is already the target type, return it
            if (targetType == value.GetType())
                return value;

            // Check whether the value is a LuaTable that must be deserialized
            var luaTable = value as LuaTable;
            if (luaTable != null)
                return DeserializeLuaTable(luaTable, targetType);

            // Use converters to convert between the returned type and the target type.
            // Prefer the value's type converter, then try the target type's converter.
            
            // Get converter for value's type
            ILuaTypeConverter converter = LuaTypeDescriptor.GetConverter(value);
            if (converter != null && converter.CanConvertTo(targetType))
                return converter.ConvertTo(value, targetType);

            // Get converter for target type
            converter = LuaTypeDescriptor.GetConverter(targetType);
            if (converter != null && converter.CanConvertFrom(value.GetType()))
                return converter.ConvertFrom(value);

            // Could not convert to target type, throw exception
            throw new ArgumentException(Strings.ValueTypeMismatchMessage, "value");

        }

        private static object DeserializeLuaTable(LuaTable table, Type targetType)
        {
            // Determine whether to deserialize the table as an array, a dictionary or a class 
            // based on the target type

            // Deserialize the table as an array
            if (targetType.IsArray)
                return DeserializeArray(table, targetType);

            if (targetType.IsDictionary())
                return DeserializeDictionary(table, targetType);

            // Deserialize the table as a class
            if (targetType.IsClass)
                return DeserializeClass(table, targetType);

            // Could not convert to target type, throw exception
            throw new ArgumentException(Strings.ValueTypeMismatchMessage, "targetType");
        }

        private static object DeserializeArray(LuaTable table, Type targetType)
        {
            // Get the type of the array elements
            Type elementType = targetType.GetElementType();

            // Create an array list with an initial capacity of the field count
            var list = new ArrayList(table.Keys.Count);

            // Iterate through the fields of the table
            foreach (DictionaryEntry field in table)
            {
                // Deserialize the value of the field and add it to the list
                list.Add(LuaSerializer.DeserializeValue(field.Value, elementType));
            }

            // Return an array holding the elements
            return list.ToArray(elementType);
        }

        private static object DeserializeDictionary(LuaTable table, Type targetType)
        {
            // Create a new instance of the dictionary type
            var dictionary = (IDictionary)Activator.CreateInstance(targetType);

            // Determine the target types
            Type keyTargetType = targetType.GetGenericArguments()[0];
            Type valueTargetType = targetType.GetGenericArguments()[1];

            // Iterate through the fields of the table
            foreach (DictionaryEntry field in table)
            {
                // Add the field by deserializing the value of the field
                dictionary.Add(LuaSerializer.DeserializeValue(field.Key, keyTargetType), LuaSerializer.DeserializeValue(field.Value, valueTargetType));
            }

            return dictionary;
        }

        private static object DeserializeClass(LuaTable table, Type targetType)
        {
            // Create a new instance of the target type
            object instance = Activator.CreateInstance(targetType);

            // Iterate through the fields of the table
            foreach (DictionaryEntry field in table)
            {
                // Check whether the field is a named field
                if (field.Key is string)
                {
                    // Find the property on the target type with the same name
                    PropertyInfo propertyInfo = targetType.GetProperty((string)field.Key);
                    if (propertyInfo != null)
                    {
                        // Set the value of the property by deserializing the value of the field
                        propertyInfo.SetValue(instance, LuaSerializer.DeserializeValue(field.Value, propertyInfo.PropertyType), null);
                    }
                }
            }

            return instance;
        }
    }
}
